package org.acooke.uykfd.db

import java.sql.Connection

import org.apache.empire.db.DBQuery
import org.apache.empire.db.DBReader

import org.jgrapht.graph.DefaultWeightedEdge
import org.jgrapht.graph.SimpleGraph
import org.jgrapht.graph.SimpleDirectedGraph
import org.jgrapht.graph.SimpleWeightedGraph

import scala.collection.JavaConversions._
import scala.collection.immutable.SortedSet
import scala.collection.immutable.TreeSet


class NoConnectionException(msg: String) extends RuntimeException(msg)


trait LfmArtistConnectionRow
  extends WeightedEdgeRow[ArtistRow] {

  def source(cnxn: Connection): ArtistRow
  def sourceId: Int
  def sink(cnxn: Connection): ArtistRow
  def sinkId: Int
  def other(node: ArtistRow, cnxn: Connection): ArtistRow
}

trait LfmArtistConnectionTable
  extends WeightedEdgeTable[
    LfmArtistConnectionRow, ArtistRow, Schema.Artists, LfmArtistConnectionTable, Schema.LfmArtistConnections
  ] {

  def allEdges(artist: ArtistRow, cnxn: Connection): List[LfmArtistConnectionRow]
  def sortedEdges(artist: ArtistRow, cnxn: Connection): SortedSet[LfmArtistConnectionRow]
  def allNeighbours(artist: ArtistRow, cnxn: Connection): List[ArtistRow]
  def unconnectedArtists(cnxn: Connection): List[ArtistRow]
  def build(cnxn:Connection)
  def toWeightedGraph(calcWeight: (Int) => Double, cnxn: Connection):
    SimpleWeightedGraph[Int, DefaultWeightedEdge]
  def toUndirectedBacklinkGraph(maxDegree: Int, cnxn: Connection):
    SimpleGraph[Int, DefaultWeightedEdge]
  def toDirectedBacklinkGraph(maxDegreeBack: Int, maxDegreeFwd: Int, cnxn: Connection):
    SimpleDirectedGraph[Int, DefaultWeightedEdge]
  def show(name: String, cnxn: Connection)
  def degree(artist: ArtistRow, cnxn: Connection): Int
}

private class LfmArtistConnectionFactory
  extends WeightedEdgeFactory[
    ArtistRow, Schema.Artists, LfmArtistConnectionRecord, LfmArtistConnectionTable, Schema.LfmArtistConnections](
    Schema.LFM_ARTIST_CONNECTIONS, Schema.ARTISTS, Schema.ARTISTS.orm.fromId)
  with Undirected[ArtistRow, Schema.Artists, LfmArtistConnectionRecord, LfmArtistConnectionTable, Schema.LfmArtistConnections]
  with LfmArtistConnectionTable {

  def rowFactory = new LfmArtistConnectionRecord(nodeFactory)

  def allEdges(artist: ArtistRow, cnxn: Connection) = fromNode(artist, cnxn)

  def sortedEdges(artist: ArtistRow, cnxn: Connection) =
    TreeSet[LfmArtistConnectionRow](fromNode(artist, cnxn).toSeq : _ *)(
      new Ordering[LfmArtistConnectionRow] {
        def compare(row1: LfmArtistConnectionRow, row2: LfmArtistConnectionRow) =
          implicitly[Ordering[Int]].compare(row2.weight, row1.weight)
      })
  
  def topEdges(artist: ArtistRow, maxDegree: Int, cnxn: Connection) = {
    val cmd = allWhere(table.SOURCE.is(artist.id).or(table.SINK.is(artist.id)))
    cmd.orderBy(table.WEIGHT, true)
    buildList(cmd, cnxn, maxDegree)
  }

  def unconnectedArtists(cnxn: Connection) = unconnectedSourceNodes(cnxn)

  def build(cnxn: Connection) = {
    erase(cnxn)
    val ac1 = Schema.ARTIST_CANONICAL
    val ac2 = Schema.ARTIST_CANONICAL.clone[ArtistCanonicalTable, Schema.ArtistCanonical]
    val t1 = Schema.LFM_ARTIST_TAGS
    val t2 = Schema.LFM_ARTIST_TAGS.clone[LfmArtistTagTable, Schema.LfmArtistTags]
    val subCmd = Schema.createCommand
    val a1 = ac1.ARTIST.as("A1")
    val w = t1.WEIGHT.max.multiplyWith(t2.WEIGHT.max).as("W1")
    val a2 = ac2.ARTIST.as("A2")
    subCmd.select(a1, w, a2)
    subCmd.join(ac1.CANONICAL, t1.SOURCE)
    subCmd.join(t1.SINK, t2.SINK)
    subCmd.join(t2.SOURCE, ac2.CANONICAL)
    // SOURCE > SINK
    subCmd.where(a2.isSmallerThan(a1))
    subCmd.groupBy(ac1.ARTIST, t1.SINK, ac2.ARTIST)
    val links = new DBQuery(subCmd)
    val cmd = Schema.createCommand
    val a1b = links.findQueryColumn(a1)
    val wb = links.findQueryColumn(w)
    val a2b = links.findQueryColumn(a2)
    cmd.select(a1b.as("SOURCE"), a2b.as("SINK"), wb.sum.as("WEIGHT"))
    cmd.groupBy(a1b, a2b)
    Schema.executeSQL(cmd.getInsertInto(Schema.LFM_ARTIST_CONNECTIONS), cnxn)
  }

  def toWeightedGraph(calcWeight: (Int) => Double, cnxn: Connection) =
    toSimpleWeightedGraph(calcWeight, cnxn)

  def toDirectedBacklinkGraph(maxDegreeBack: Int, maxDegreeFwd: Int, cnxn: Connection) = {
    val graph = new SimpleDirectedGraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge])
    toBacklinkGraph(cnxn, (artist: ArtistRow) => {
      def add(source: Int, sink: Int) = {
        if (! graph.containsVertex(source)) graph.addVertex(source)
        if (! graph.containsVertex(sink)) graph.addVertex(sink)
        if (! graph.containsEdge(source, sink)) graph.addEdge(source, sink)
      }
      for (edge <- topEdges(artist, maxDegreeBack, cnxn))
        add(edge.other(artist, cnxn).id, artist.id)
      for (edge <- topEdges(artist, maxDegreeFwd, cnxn))
        add(artist.id, edge.other(artist, cnxn).id)
    })
    graph
  }

  def toUndirectedBacklinkGraph(maxDegree: Int, cnxn: Connection) = {
    val graph = new SimpleGraph[Int, DefaultWeightedEdge](classOf[DefaultWeightedEdge])
    toBacklinkGraph(cnxn, (artist: ArtistRow) =>
      for (edge <- topEdges(artist, maxDegree, cnxn)) {
        if (! graph.containsVertex(edge.sourceId)) graph.addVertex(edge.sourceId)
        if (! graph.containsVertex(edge.sinkId)) graph.addVertex(edge.sinkId)
        if (! graph.containsEdge(edge.sourceId, edge.sinkId))
          graph.addEdge(edge.sourceId, edge.sinkId)
      })
    graph
  }

  def toBacklinkGraph(cnxn: Connection, builder: (ArtistRow) => Unit) = {
    val reader = new DBReader
    try {
      val cmd1 = Schema.createCommand
      cmd1.select(table.SOURCE)
      cmd1.selectDistinct
      val cmd2 = Schema.createCommand
      cmd2.select(table.SINK.as("SOURCE"))
      cmd2.selectDistinct
      val cmd = cmd1.union(cmd2)
      reader.open(cmd, cnxn)
      while (reader.moveNext)
        builder(Schema.ARTISTS.orm.fromId(Record.asInt(reader, table.SOURCE), cnxn))
    } finally {
      reader.close
    }
  }

  def show(name: String, cnxn: Connection) =
    Schema.ARTISTS.orm.lookupValue(name, cnxn) match {
      case Some(artist) =>
        for (edge <- fromNode(artist, cnxn))
          yield println(edge.other(artist, cnxn).value + " " + edge.weight)
      case None => println(name + " not found")
    }

}

private class LfmArtistConnectionRecord(nf: (Int, Connection) => ArtistRow)
  extends WeightedEdgeRecord[
    ArtistRow, Schema.Artists, LfmArtistConnectionRecord, LfmArtistConnectionTable, 
    Schema.LfmArtistConnections
  ](Schema.LFM_ARTIST_CONNECTIONS, nf)
  with LfmArtistConnectionRow
